{$M 3072,0,15360}
{
	3k heap + 15k stack + 35k .exe
	equals 53k memory footprint
}

program paku;

{
	PAKU PAKU, A TXTGRAPH.PAS DEMO
	Jason M. Knight, 2011

	deathshadow60@hotmail.com
	http://my.opera.com/deathshadow/blog/
	http://www.deathshadow.com

	TXTGRAPH.PAS was created as a Retrochallenge Winter Warmup 2011 Entry
	http://www.wickensonline.co.uk/retrochallenge-website/index.html
}

uses
  jfunc,
  timer,
  sound,
  txtGraph;

const
	revision='1.0';
	buildDate='8 February, 2011';

type
  gameSprite=object
    tile:integer;
    cSprite:pSprite;
    constructor init(sx,sy,sTile:integer);
  end;

  tBonus=object(gameSprite)
    showCounter,
    direction:integer;
    constructor init;
    procedure reset;
    procedure activate;
    procedure update;
  end;

  tPlayer=object(gameSprite)
    direction,inputDirection,waka,wakaDir:integer;
    lastInputDirX:boolean;
    constructor init;
    procedure reset;
    procedure update;
  end;

  pGhost=^tGhost;
  tGhost=object(gameSprite)
    direction,inputDirection,
    tileOffset,activeTileOffset,
    logic,
    mode,
    frame,
    fleeCounter,
    exitCounter,
    homeX,homeY:integer;
    alternate,inJail:boolean;
    constructor init(sLogic:integer);
    procedure reset;
    procedure update;
  end;

  tGhostData=record
    nickname,behavior:string[8];
    color:byte;
    cx,cy,d,hx,hy:integer;
  end;

  tLevelData=record
    symbol,points,rate,frightTime,flashes:integer;
  end;

  tBehavior=record
    behavior,time:word;
  end;
  tBehaviorList=array[0..7] of tBehavior;

  tPelletData=record
    x,y,
    tileX,
    tileY:word;
  end;

  tTextLine=record
    x,y:word;
    data:string;
  end;

const
  titleString=#13+#10+'PAKU PAKU By Jason M. Knight, Paladin Systems North. '+#13+#10+
    'Version '+revision+' - '+buildDate+#13+#10;

  { difficulty settings }
  pointsPerLife=10000;
  startingLives=3;
  startingLevel=0;

  {game settings}
  startingDots=244;
  fruitCounter1=startingDots-70;
  fruitCounter2=fruitCounter1-100;
  elroy=20;
  superElroy=10;

  move_up    = 0;
  move_right = 1;
  move_down  = 2;
  move_left  = 3;

  mode_scatter = 0;
  mode_chase   = 1;
  mode_flee    = 2;
  mode_eaten   = 3;

  jail_left   = 33;
  jail_right  = 52;
  jail_top    = 38;
  jail_bottom = 44;

  blinky=0;
  pinky=1;
  inky=2;
  clyde=3;

  fieldOffsetX=1;
  fieldWidth=28;
  fieldEndX=fieldWidth*3+fieldOffsetX;

  sirenLookup:array[0..23] of word=(
    440,466,494,523,554,587,622,659,698,740,784,831,
    880,831,784,740,698,659,622,587,554,523,494,466
  );

  ghostData:array[0..3] of tGhostData=(
    (nickname:'Stinky'; behavior:'Hound';   color:4; cX:40; cY:32; d:move_left; hX:fieldWidth*3; hY:5),
    (nickname:'Kinky';  behavior:'Hunter';  color:5; cX:41; cY:39; d:move_up;   hX:1;            hY:5),
    (nickname:'Hinky';  behavior:'Schizo';  color:3; cX:34; cY:39; d:move_down; hX:fieldWidth*3; hY:85),
    (nickname:'Blaine'; behavior:'Elusive'; color:6; cX:47; cY:39; d:move_up;   hX:1; hY:85)
  );

  levelData:array[1..21] of tLevelData=(
    (symbol:$38; points:100;  rate:6; frightTime:1320; flashes:600),
    (symbol:$39; points:300;  rate:5; frightTime:1200; flashes:600),
    (symbol:$3A; points:500;  rate:5; frightTime:1080; flashes:600),
    (symbol:$3A; points:500;  rate:5; frightTime:960;  flashes:600),

    (symbol:$3B; points:700;  rate:4; frightTime:840;  flashes:600),
    (symbol:$3B; points:700;  rate:4; frightTime:1320; flashes:600),
    (symbol:$3C; points:1000; rate:4; frightTime:840;  flashes:600),
    (symbol:$3C; points:1000; rate:4; frightTime:840;  flashes:600),

    (symbol:$3D; points:2000; rate:4; frightTime:480;  flashes:360),
    (symbol:$3D; points:2000; rate:4; frightTime:1200; flashes:600),
    (symbol:$3E; points:3000; rate:4; frightTime:840;  flashes:600),
    (symbol:$3E; points:3000; rate:4; frightTime:480;  flashes:360),

    (symbol:$3F; points:5000; rate:4; frightTime:480; flashes:360),
    (symbol:$3F; points:5000; rate:4; frightTime:960; flashes:600),
    (symbol:$3F; points:5000; rate:4; frightTime:480; flashes:360),
    (symbol:$3F; points:5000; rate:4; frightTime:480; flashes:360),

    (symbol:$3F; points:5000; rate:4; frightTime:0;   flashes:0),
    (symbol:$3F; points:5000; rate:4; frightTime:480; flashes:360),
    (symbol:$3F; points:5000; rate:4; frightTime:0;   flashes:0),
    (symbol:$3F; points:5000; rate:4; frightTime:0;   flashes:0),

    (symbol:$3F; points:5000; rate:5; frightTime:0;   flashes:0)
  );

  behaviorLevel1:tBehaviorList=(
    (behavior:mode_scatter; time:420),
    (behavior:mode_chase; time:1200),
    (behavior:mode_scatter; time:420),
    (behavior:mode_chase; time:1200),
    (behavior:mode_scatter; time:300),
    (behavior:mode_chase; time:1200),
    (behavior:mode_scatter; time:300),
    (behavior:mode_chase; time:0)
  );
  behaviorLevel2_4:tBehaviorList=(
    (behavior:mode_scatter; time:420),
    (behavior:mode_chase; time:1200),
    (behavior:mode_scatter; time:420),
    (behavior:mode_chase; time:1200),
    (behavior:mode_scatter; time:300),
    (behavior:mode_chase; time:65535),
    (behavior:mode_scatter; time:1),
    (behavior:mode_chase; time:0)
  );
  behaviorLevel5Plus:tBehaviorList=(
    (behavior:mode_scatter; time:300),
    (behavior:mode_chase; time:1200),
    (behavior:mode_scatter; time:300),
    (behavior:mode_chase; time:1200),
    (behavior:mode_scatter; time:300),
    (behavior:mode_chase; time:65535),
    (behavior:mode_scatter; time:1),
    (behavior:mode_chase; time:0)
  );

  themeBass:string[99]='VF,'+
    '1B2,R4,2B2,'+ {8}
    '1B2,R4,2B2,'+ {16}
    '1C2,R4,2C2,'+ {24}
    '1C2,R4,2C2,'+ {32}
    '1B2,R4,2B2,'+ {40}
    '1B2,R4,2B2,'+ {48}
    '1F#2,R2,1G#2,R2,'+ {56}
    '2A#2,R2,2B2,R2'; {64}

  themeLead:string[203]='VF,'+
    '2B1,R1,3B1,R1,2F#1,R1,2D#1,R1,'+   {8}
    '3B1,2F#1,R2,2D#3,R1,'+             {16}
    '2C1,R1,3C1,R1,2G1,R1,2E1,R1,'+     {24}
    '3C1,2G1,R2,2E3,R1,'+               {32}
    '2B1,R1,3B1,R1,2F#1,R1,2D#1,R1,'+   {40}
    '3B1,2F#1,R2,2D#3,R1,'+             {48}
    '2D#1,2E1,2F1,R1,2F1,2F#1,2G1,R1,'+ {56}
    '2G1,2G#1,3A1,R1,3B2,R2';           {64}

  pelletData:array[0..3] of tPelletData=(
    (x:4;  y:9;  tileX:1;  tileY:3),
    (x:79; y:9;  tileX:26; tileY:3),
    (x:79; y:69; tileX:26; tileY:23),
    (x:4;  y:69; tileX:1;  tileY:23)
  );

  borderColors:array[0..2] of byte=(
    15,12,4
  );

  menuText:array[0..5] of tTextLine=(
    (x:87;  y:1;  data:'High Score'),
    (x:87;  y:7;  data:'Last Score'),
    (x:107; y:62; data:'10 points'),
    (x:107; y:68; data:'50 points'),
    (x:97;  y:77; data:'ENTER to play'),
    (x:102; y:83; data:'ESC to exit')
  );

var
  score,
  highScore:longint;

  lives,level,truncLevel,
  dots,
  ghostMode,
  nextGhost,
  behaviorStep,
  behaviorCounter,
  globalDotCounter,
  globalJailCounter,
  globalFleeCounter,
  pelletBlinkCounter,
  gainLifeCounter,
  fleeTotal,
  siren,pakuSound,
  frameSpeed,
  timerInterval:word;

  speedTestResult:longint;

  soundOn,vSync,
  globalJailCounterEnable:boolean;

  map,
  mapTiles,
  spriteTiles:tileset;


  fonts:fontSet;
  numbers:numberSet;

  tileMap:array[0..30,0..fieldWidth-1] of byte;

  player:tPlayer;
  ghost:array[0..3] of tGhost;
  ghostScoreBg:array[0..3] of pointer;
  bonus:tBonus;

procedure waitNoReset(ticks:word);
begin
  repeat
    if userTimerExpired(timerInterval) then dec(ticks);
  until ticks<=0;
end;

procedure wait(ticks:word);
begin
  userClockCounter:=0;
  waitNoReset(ticks);
end;

procedure playTheme;
var
  lead,bass:tSynthLine;
begin
  lead.init(@themeLead,100,0,1);
  bass.init(@themeBass,100,1,0);
  wait(20);
  userClockCounter:=0;
  repeat
    if userTimerExpired(timerInterval) then begin
      lead.playStep;
      bass.playStep;
    end;
  until (lead.done and bass.done);
end;

procedure killVoices;
var
  t:word;
begin
  for t:=0 to 2 do outFreq(t,0,0,0);
end;

procedure playEatGhost;
var
  count:word;
begin
  if (soundOn) then begin
    count:=110;
    killVoices;
    while count<1760 do begin
      if userTimerExpired(timerInterval) then begin
        outFreq(0,count,$0F,0);
        inc(count,25);
      end;
    end;
  end;
end;

procedure colorize(sx,sy,ex,ey:integer; color,ignore:byte);
var
  x,y:integer;
begin
  for y:=sy to ey do begin
    for x:=sx to ex do begin
      if not(tg_getPixel(x,y)=ignore) then tg_putpixel(x,y,color);
    end;
  end;
end;

procedure showAudioStatus;
begin
  tg_bar(87,94,122,98,0);
  fonts.outText(87,94,'SOUND');
  colorize(91,94,93,98,15,0);
  if soundOn then begin
    fonts.outtext(109,94,'ON');
    colorize(109,94,122,98,10,0);
  end else begin
    fonts.outtext(109,94,'OFF');
    colorize(109,94,122,98,4,0);
  end;
end;

procedure showVSyncStatus;
begin
  tg_bar(126,94,159,98,0);
  fonts.outtext(126,94,'VSYNC');
  colorize(126,94,128,98,15,0);
  if vSync then begin
    fonts.outtext(148,94,'ON');
    colorize(148,94,159,98,10,0);
  end else begin
    fonts.outtext(148,94,'OFF');
    colorize(148,94,159,98,4,0);
  end;
end;

procedure showLives;
var
  t,x:word;
begin
  t:=1;
  x:=2;
  while (t<lives) do begin
    tg_tile5(x,94,spriteTiles.dataStart,$4C);
    inc(x,6);
    inc(t);
  end;
  tg_bar(x,94,26,99,0);
end;

procedure scorePoints(points:longint);
var
  oldScore:longint;
begin
  oldScore:=score;
  score:=score+points;
  numbers.outDecimal(108,36,score,1);
  if (score>highScore) then begin
    highScore:=score;
    numbers.outDecimal(108,18,highScore,1);
  end;
  if (oldScore mod pointsPerLife)>(score mod pointsPerLife) then begin
    if (lives<5) then begin
    	inc(lives);
    	gainLifeCounter:=90;
    end;
    showLives;
  end;
end;

constructor tBonus.init;
begin
  showCounter:=0;
  direction:=move_right;
  cSprite:=tg_addSprite(150,90,$5F,5,@spriteTiles);
  tBonus.reset;
end;

procedure tBonus.reset;
begin
  with cSprite^ do begin
    currentX:=154;
    currentY:=0;
    currentTile:=$5F;
    showCounter:=0;
  end;
end;

procedure tBonus.activate;
begin
  with (cSprite^) do begin
    cSprite^.currentTile:=levelData[truncLevel].symbol;
    currentX:=40;
    currentY:=50;
  end;
  showCounter:=(1080+random(120)) div frameSpeed;
end;

procedure tBonus.update;
begin
  if (showCounter>0) then begin
    dec(showCounter);
  end else if not(cSprite^.currentTile=$5F) then reset;
end;

constructor gameSprite.init(sx,sy,sTile:integer);
begin
  tile:=sTile;
  cSprite:=tg_addSprite(sx,sy,tile,5,@spriteTiles);
end;

constructor tPlayer.init;
begin
  gameSprite.init(40,68,$42);
  reset;
end;

procedure tPlayer.reset;
begin
  with cSprite^ do begin
    currentX:=40;
    currentY:=68;
    currentTile:=$4C;
  end;
  direction:=3;
  inputdirection:=3;
  waka:=0;
end;

procedure tPlayer.update;
var
  mx,my,
  tx,ty,
  dCount,oldDirection:integer;
  t:word;

  procedure eatDot;
  begin
    tileMap[ty,tx]:=$28;
    if (soundSource=sound_pcSpeaker) then begin
    	pakuSound:=6;
    end else begin
    	pakuSound:=8;
    end;
    scorePoints(10);
    tg_eraseUnderSprite(tx*3+1,ty*3,tx*3+3,ty*3+2);
    dec(dots);
    globalDotCounter:=0;
    if globalJailCounterEnable then begin
      inc(globalJailCounter);
      if globalJailCounter>7 then begin
        ghost[2].exitCounter:=0;
        if globalJailCounter>14 then begin
          ghost[3].exitCounter:=0;
        end;
      end;
    end else begin
      t:=0;
      repeat
        with ghost[t] do begin
          if inJail and (exitCounter>0) then begin
            dec(exitCounter);
            t:=4;
          end else inc(t);
        end;
      until t>=4;
    end;
  end;

begin
  with (cSprite^) do begin

    dCount:=1;
    oldDirection:=direction;

    mx:=currentX mod 3;
    my:=(currentY+1) mod 3;

    tx:=(currentX+1) div 3;
    ty:=(currentY+1) div 3;

    if (
      (direction=move_left) and
      (currentX=0) and
      (currentY=41)
    ) then begin
      currentX:=81;
    end else if (
      (direction=move_right) and
      (currentX=81) and
      (currentY=41)
    ) then begin
      currentX:=0;
    end;

    case tileMap[ty,tx] of
      $20:eatDot;
      $21:begin
        eatDot;
        globalFleeCounter:=levelData[truncLevel].frightTime;
        nextGhost:=200;
        for t:=0 to 3 do with ghost[t] do begin
          if (
            not(inJail) and
            not(mode=mode_eaten)
          ) then begin
            mode:=mode_flee;
            direction:=(direction+2) mod 4;
            fleeCounter:=globalFleeCounter;
          end;
        end;
      end;
    end;

    case direction of
      move_up,move_down:begin
        case inputDirection of
          move_up,move_down:direction:=inputDirection;
          else if (my=0) then begin
            case inputDirection of
              move_right:if tileMap[ty,tx+1]>0 then direction:=1;
              move_left:if tileMap[ty,tx-1]>0 then direction:=3;
            end;
          end;
        end;
      end;
      move_left,move_right:begin
        case inputDirection of
          move_left,move_right:direction:=inputDirection;
          else if (mx=0) then begin
            case inputDirection of
              move_up:if tileMap[ty-1,tx]>0 then direction:=0;
              move_down:if tileMap[ty+1,tx]>0 then direction:=2;
            end;
          end;
        end;
      end;
    end;

    if not(direction=oldDirection) then inc(dCount);
    case direction of
      move_up:if my=0 then begin
        if tileMap[ty-1,tx]>0 then dec(currentY,dCount) else waka:=4;
      end else dec(currentY);
      move_right:if mx=0 then begin
        if tileMap[ty,tx+1]>0 then inc(currentX,dCount) else waka:=4;
      end else inc(currentX);
      move_down:if my=0 then begin
        if tileMap[ty+1,tx]>0 then inc(currentY,dCount) else waka:=4;
      end else inc(currentY);
      move_left:if mx=0 then begin
        if tileMap[ty,tx-1]>0 then dec(currentX,dCount) else waka:=4;
      end else dec(currentX);
    end;

    currentTile:=$40+direction*4+waka div 2;
  end;

  waka:=(waka+1) mod 8;

end;

constructor tGhost.init(sLogic:integer);
begin
  logic:=sLogic;
  cSprite:=tg_addSprite(0,0,0,5,@spriteTiles);
  reset;
end;

procedure tGhost.reset;
begin
  tileOffset:=logic*8;
  activeTileOffset:=tileOffset;
  frame:=0;
  alternate:=false;
  with cSprite^ do with ghostData[logic] do begin
    direction:=d;
    homeX:=hx;
    homeY:=hy;
    currentX:=cx;
    currentY:=cy;
    mode:=mode_scatter;
    currentTile:=activeTileOffset+(frame div 4)+direction*2;
    exitCounter:=0;
    case logic of
      2:if level=1 then inc(exitCounter,30);
      3:if level=1 then inc(exitCounter,60) else inc(exitCounter,50);
    end;
  end;
  inputDirection:=direction;
end;

procedure tGhost.update;
var
  dx,dy,
  adx,ady,
  sdx,sdy,
  mx,my,
  tx,ty,
  testAmt,
  r10,
  howFar:integer;
  allowTurn,jailSkip:boolean;
begin
  r10:=random(10);
  alternate:=not(alternate);

  with (cSprite^) do begin

    mx:=currentX mod 3;
    my:=(currentY+1) mod 3;

    tx:=(currentX+1) div 3;
    ty:=(currentY+1) div 3;

    inJail:=(
      (currentX>=jail_left) and
      (currentX<=jail_right) and
      (currentY>=jail_top) and
      (currentY<=jail_bottom)
    );

    if (inJail) then begin

      if (mode=mode_eaten) and (currentY<jail_bottom) then begin
        inc(currentY);
        if (currentY=jail_bottom) then begin
          mode:=mode_scatter;
        end;
      end else begin

        if exitCounter>0 then case currentY of
          jail_top:begin
            direction:=move_down;
            inc(currentX);
          end;
          jail_bottom:begin
            direction:=move_up;
            dec(currentX);
          end;
        end else begin
          case currentX of
            jail_left..39:direction:=move_right;
            41..jail_right:direction:=move_left;
            40:direction:=move_up;
          end;
        end;

        case direction of
          move_up:dec(currentY);
          move_right:inc(currentX);
          move_down:inc(currentY);
          move_left:dec(currentX);
        end;
      end;

    end else begin

      if (
        (direction=move_left) and
        (currentX=0) and
        (currentY=41)
      ) then begin
        currentX:=81;
      end else if (
        (direction=move_right) and
        (currentX=81) and
        (currentY=41)
      ) then begin
        currentX:=0;
      end;

      howFar:=1;
      jailSkip:=false;

      case mode of
        mode_scatter:begin
          dx:=homeX-currentX;
          dy:=homeY-currentY;
        end;
        mode_chase:begin
          case logic of
            blinky:begin
              dx:=player.cSprite^.currentX-currentX;
              dy:=player.cSprite^.currentY-currentY;
            end;
            pinky:begin
              case player.direction of
                move_up:begin
                  dx:=player.cSprite^.currentX-12;
                  dy:=player.cSprite^.currentY-12;
                end;
                move_down:begin
                  dx:=player.cSprite^.currentX;
                  dy:=player.cSprite^.currentY+12;
                end;
                move_left:begin
                  dx:=player.cSprite^.currentX-12;
                  dy:=player.cSprite^.currentY;
                end;
                move_right:begin
                  dx:=player.cSprite^.currentX+12;
                  dy:=player.cSprite^.currentY;
                end;
              end;
              dx:=dx-currentX;
              dy:=dy-currentY;
            end;
            inky:begin
              case player.direction of
                move_up:begin
                  dx:=player.cSprite^.currentX-6;
                  dy:=player.cSprite^.currentY-6;
                end;
                move_down:begin
                  dx:=player.cSprite^.currentX;
                  dy:=player.cSprite^.currentY+6;
                end;
                move_left:begin
                  dx:=player.cSprite^.currentX-6;
                  dy:=player.cSprite^.currentY;
                end;
                move_right:begin
                  dx:=player.cSprite^.currentX+6;
                  dy:=player.cSprite^.currentY;
                end;
              end;
              dx:=((dx+ghost[blinky].cSprite^.currentX) div 2)-currentX;
              dy:=((dy+ghost[blinky].cSprite^.currentY) div 2)-currentY;
            end;
            clyde:begin
              dx:=player.cSprite^.currentX-currentX;
              dy:=player.cSprite^.currentY-currentY;
              testAmt:=round(sqrt(dx*dx+dy*dy));
              if (testAmt<7) then begin
                dx:=homeX-currentX;
                dy:=homeY-currentY;
              end;
            end;
          end;
        end;
        mode_flee:begin
          dx:=currentX-player.cSprite^.currentX;
          dy:=currentY-player.cSprite^.currentY;
          dec(fleeCounter,frameSpeed);
          if (fleeCounter<=0) then mode:=ghostMode;
        end;
        mode_eaten:begin
          if (
            ((currentX=40) or (currentX=41)) and
            (currentY>=31) and
            (currentY<jail_bottom)
          ) then begin
            direction:=move_down;
            jailSkip:=true;
          end else begin
            dx:=40-currentX;
            dy:=32-currentY;
            inc(howFar);
          end;
        end;
      end;

      if not(jailSkip) then begin

        adx:=abs(dx);
        ady:=abs(dy);

        case direction of
          move_up,move_left:testAmt:=-1;
          else testAmt:=1;
        end;

        case direction of

          move_up,move_down:if (my=0) then begin
            allowTurn:=(
              (r10>1) and
              ((currentX=27) or (currentX=54)) and
              (currentY>26) and
              (currentY<56)
            );
            if (
              (adx>ady) or
              (tileMap[ty+testAmt,tx]=0) or
              (dy>0) or
              allowTurn
              or (r10>0)
            ) then begin
              if dx>0 then begin
                if tileMap[ty,tx+1]>0 then begin
                  direction:=move_right;
                end else if (
                  (tileMap[ty,tx-1]>0) and
                  ((tileMap[ty+testAmt,tx]=0) or (allowTurn and (currentX=54)))
                ) then begin
                  direction:=move_left;
                end;
              end else begin
                if tileMap[ty,tx-1]>0 then begin
                  direction:=move_left;
                end else if (
                  (tileMap[ty,tx+1]>0) and
                  ((tileMap[ty+testAmt,tx]=0) or (allowTurn and (currentX=27)))
                ) then begin
                  direction:=move_right;
                end;
              end;
            end;
          end;

          move_left,move_right:if (mx=0) then begin
            allowTurn:=not(
              (r10>0) and
              ((currentX>27) and (currentX<54)) and
              ((currentY=32) or (currentY=68))
            );
            if (ady>adx) or (tileMap[ty,tx+testAmt]=0) or (dx>0) or (r10>0) then begin
              if dy>0 then begin
                if tileMap[ty+1,tx]>0 then begin
                  direction:=move_down;
                end else if (
                  (tileMap[ty-1,tx]>0) and
                  (tileMap[ty,tx+testAmt]=0) and
                  allowTurn
                ) then begin
                  direction:=move_up;
                end;
              end else begin
                if (tileMap[ty-1,tx]>0) and allowTurn then begin
                  direction:=move_up;
                end else if (tileMap[ty+1,tx]>0) and (tileMap[ty,tx+testAmt]=0) then begin
                  direction:=move_down;
                end;
              end;
            end;
          end; { left/right }

        end; {case }

      end; { not jailskip }

      if not(
        (
          (mode=mode_flee) or (
            (currentY=41) and (
              (currentX<18) or (currentX>62)
            )
          )
        ) and alternate
      )
       then case direction of
        move_up:if my=0 then begin
          if tileMap[ty-1,tx]>0 then dec(currentY,howfar);
        end else dec(currentY);
        move_right:if mx=0 then begin
          if tileMap[ty,tx+1]>0 then inc(currentX,howfar);
        end else inc(currentX);
        move_down:if my=0 then begin
          if (tileMap[ty+1,tx]>0) or jailSkip then inc(currentY,howfar);
        end else inc(currentY);
        move_left:if mx=0 then begin
          if tileMap[ty,tx-1]>0 then dec(currentX,howfar);
        end else dec(currentX);
      end;

    end;
    case mode of
      mode_eaten:currentTile:=$30+(frame div 4)+direction*2;
      mode_flee:if (
        (fleeCounter<levelData[truncLevel].flashes) and
        ((fleeCounter mod 120) div 60=0)
      ) then begin
        currentTile:=$28+(frame div 4)+direction*2;
      end else currentTile:=$20+(frame div 4)+direction*2;
      else currentTile:=activeTileOffset+(frame div 4)+direction*2;
    end;
  end;
  frame:=(frame+1) mod 8;
end;

procedure setupTileMap;
var
  px,py,t:word;
  mapOffset,storeOffset:pTileData;
  c:byte;
begin
  mapOffset:=map.dataStart;
  storeOffset:=pTileData(@tileMap);
  while not(mapOffset=map.dataEnd) do begin
    if (mapOffset^ and $80)=$80 then begin
      c:=mapOffset^ and $7F;
      inc(mapOffset);
    end else c:=1;
    while c>0 do begin
      case mapOffset^ of
        $20,$21,$28:storeOffset^:=mapOffset^;
        else storeOffset^:=0;
      end;
      inc(storeOffset);
      dec(c);
    end;
    inc(mapOffset);
  end;
end;

procedure loadData;
var
  t:word;
  l:longint;
begin
  if (memAvail<14848) then begin
  	writetext('memory');
    halt;
  end;
  writeln('Loading Data');
  map.init('MAP');
  mapTiles.init('MAPTILES');
  spriteTiles.init('SPRITES');
  fonts.init('FONTS');
  numbers.init('HEXNUM');
  setupTileMap;
  writeln('Initializing Sprites System');
  for t:=0 to 3 do begin
    ghost[t].init(t);
    getmem(ghostScoreBg[t],30);
  end;
  player.init;
  bonus.init;
  writeln;
  write('Starting Timer');
  startTimer;
  timerInterval:=getUserClockInterval(120);
  writeln(' - Complete');
  write('Starting Sound');
  startSound;
  writeln(' - Complete');
  if paramExists('/debug') then begin
  	writeln;
  	writeln('Dos Free:',dosAvail);
  	writeln('Heap Free:',memAvail);
  	writeln('Stack Free:',stackAvail);
  	writeln;
  	writeln('Press any key to continue');
  	waitkey;
  end;
end;

procedure cleanup;
var
  t:word;
begin
  write('Halted: Graphics');
  killSound;
  write(' Sound');
  killTimer;
  write(' Timer - Releasing Memory');
  for t:=0 to 3 do begin
    freemem(ghostScoreBg[t],30);
  end;
  numbers.term;
  fonts.term;
  spriteTiles.term;
  mapTiles.term;
  map.term;
  writeln(' - Complete');
end;

procedure showReady;
var
  t,n:word;
begin
  n:=32;
  for t:=0 to 5 do begin
    tg_tile5(n,50,spriteTiles.dataStart,$58+t);
    inc(n,5);
  end;
end;

procedure eraseReady;
begin
  tg_bar(32,50,56,54,0);
end;

{ map is stored RLE, so we need to decode that }
procedure drawPlayfield;
var
  px,py:word;
  mapOffset:pTileData;
  c:byte;

begin
  px:=fieldOffsetX;
  py:=0;
  mapOffset:=map.dataStart;
  while not(mapOffset=map.dataEnd) do begin
    if (mapOffset^ and $80)=$80 then begin
      {
        bit 7 set, bottom bits holds how many times
        to repeat next byte. Store the repeat and move
        the pointer ahead one.
      }
      c:=mapOffset^ and $7F;
      inc(mapOffset);
    end else c:=1; { otherwise it's just 1 raw data byte }
    while (c>0) do begin
      tg_tile3(px,py,mapTiles.dataStart,mapOffset^);
      inc(px,3);
      if (px>=fieldEndX) then begin
        px:=fieldOffsetX;
        inc(py,3);
        if (py>93) then py:=0;
      end;
      dec(c);
    end;
    inc(mapOffset);
  end;
end;

procedure redrawPlayfield;
var
  px,py:word;
  mapOffset:pTileData;
  c:byte;

begin
  px:=fieldOffsetX;
  py:=0;
  mapOffset:=map.dataStart;
  while not(mapOffset=map.dataEnd) do begin
    if (mapOffset^ and $80)=$80 then begin
      {
        bit 7 set, bottom bits holds how many times
        to repeat next byte. Store the repeat and move
        the pointer ahead one.
      }
      c:=mapOffset^ and $7F;
      inc(mapOffset);
    end else c:=1; { otherwise it's just 1 raw data byte }
    while (c>0) do begin
      if not(mapOffset^=$20) and not(mapOffset^=$21) then begin
        tg_tile3(px,py,mapTiles.dataStart,mapOffset^);
      end;
      inc(px,3);
      if (px>=fieldEndX) then begin
        px:=fieldOffsetX;
        inc(py,3);
        if (py>93) then py:=0;
      end;
      dec(c);
    end;
    inc(mapOffset);
  end;
end;

procedure whitePlayField; assembler;
asm
  mov  cx,$3AC0
  mov  ax,$B800
  mov  es,ax
  mov  di,1
  mov  dx,$0301
  mov  bx,$3010
@loop:
  mov  al,es:[di]
  mov  ah,al
  and  ah,dh
  cmp  ah,dl
  jne  @compare2
  or   al,$07
@compare2:
  mov  ah,al
  and  ah,bh
  cmp  ah,bl
  jne  @writeIt
  or   al,$70
@writeIt:
  stosb
  inc  di
  loop @loop
end;

procedure resetSprites;
var
  t:word;
begin
  for t:=0 to 3 do ghost[t].reset;
  player.reset;
end;

procedure setupPlayfield;
var
  t,n,x:word;
  b:byte;
begin
  resetSprites;
  setupTileMap;
  tg_clear(0);
  drawPlayField;
  fonts.outtext(104,12,'High Score');
  fonts.outtext(104,30,'Your Score');
  numbers.outDecimal(108,36,score,1);
  numbers.outDecimal(108,18,highScore,1);
  x:=78;
  t:=level;
  if (t>9) then begin
    n:=9;
  end else n:=t;
  while (n>0) do begin
    if (t>21) then begin
      b:=levelData[21].symbol;
    end else b:=levelData[t].symbol;
    tg_tile5(x,94,spriteTiles.dataStart,b);
    x:=x-6;
    dec(n);
    dec(t);
  end;
  showLives;
  showAudioStatus;
  showVSyncStatus;
  tg_showSprites;
end;

procedure behaviorUpdate;
var
  t:word;
begin
  case level of
    1:begin
      behaviorCounter:=behaviorLevel1[behaviorStep].time;
      ghostMode:=behaviorLevel1[behaviorStep].behavior;
    end;
    2..4:begin
      behaviorCounter:=behaviorLevel2_4[behaviorStep].time;
      ghostMode:=behaviorLevel2_4[behaviorStep].behavior;
    end;
    else begin
      behaviorCounter:=behaviorLevel5Plus[behaviorStep].time;
      ghostMode:=behaviorLevel5Plus[behaviorStep].behavior;
    end;
  end;
  for t:=0 to 3 do with ghost[t] do begin
    if (
      not(inJail) and
      not(mode=mode_flee) and
      not(mode=mode_eaten)
    ) then mode:=ghostMode;
  end;
  if (behaviorStep<7) then inc(behaviorStep);
end;

procedure eatGhostScore(x,y,points:integer);
var
  t,x1,x2:integer;
begin
  scorePoints(points);
  x1:=x-2;
  if (points>999) then dec(x1,2);
  x2:=x1;
  for t:=0 to 2 do begin
    tg_getBackground5(x1,y,ghostScoreBg[t]);
    inc(x1,6);
  end;
  numbers.outDecNonPadded(x2,y,points,1);
  playEatGhost;
  for t:=0 to 2 do begin
    tg_restoreBackground5(x2,y,ghostScoreBg[t]);
    inc(x2,6);
  end;
end;

procedure testCollisions;
var
  dx,dy,sx,sy,ex,ey:integer;
  t,n:word;
begin

  with bonus.cSprite^ do begin
    sx:=currentX-2;
    ex:=currentX+2;
    sy:=currentY-2;
    ey:=currentY+2;
  end;

  with player.cSprite^ do begin

    if (
      (bonus.showCounter>0) and
      (currentX>sx) and
      (currentX<ex) and
      (currentY>sy) and
      (currentY<ey)
    ) then begin
      bonus.reset;
      eatGhostScore(currentX,currentY,levelData[truncLevel].points);
    end;
  end;

  for t:=0 to 3 do with ghost[t] do begin
    dx:=abs(cSprite^.currentX-player.cSprite^.currentX);
    if (dx<2) then begin
      dy:=abs(cSprite^.currentY-player.cSprite^.currentY);
      if (dy<2) then begin
        case mode of
          mode_flee:with cSprite^ do begin
            eatGhostScore(currentX,currentY,nextGhost);
            nextGhost:=nextGhost*2;
            mode:=mode_eaten;
          end;
          mode_chase,mode_scatter:begin
            if soundOn then killVoices;
            tg_hideSprites;
            with player.cSprite^ do begin
              tg_getBackground5(currentX,currentY,ghostScoreBg[0]);
            end;
            n:=0;
            repeat
              if userTimerExpired(timerInterval) then begin
                if soundOn then begin
                  case n of
                    0..80:outFreq(0,880-n*5+trunc(220*sin(n/1.5)),$0B,0);
                    85,100:outFreq(0,0,0,0);
                    90,110:outFreq(0,55,$0F,0);
                  end;
                end;
                with player.cSprite^ do begin
                  if vSync then tg_waitRetrace;
                  tg_restoreBackground5(currentX,currentY,ghostScoreBg[0]);
                  tg_tile5(
                    currentX,currentY,
                    spriteTiles.dataStart,
                    $50+n div 15
                  );
                end;
                inc(n);
              end;
            until n>=120;
            if soundOn then outFreq(0,0,0,0);
            wait(45);
            with player.cSprite^ do begin
              tg_restoreBackground5(currentX,currentY,ghostScoreBg[0]);
            end;
            dec(lives);
            showLives;
            if (lives>0) then begin
              resetSprites;
              bonus.reset;
              tg_showSprites;
              showReady;
              wait(120);
              eraseReady;
            end else begin
              fonts.outtextCentered(44,40,'Game Over');
              wait(240);
            end;
          end;
        end;
      end;
    end;
  end
end;

procedure graphText(y:word; section:st20; bgColor:byte);
var
	f:text;
	tst:st80;
	compareSt:st20;
	yy:word;
	cLine:boolean;
begin
	tg_clear(bgColor);
	assign(f,'TEXTS.TDT');
	reset(f);
	compareSt:='@'+str2Lower(section);
	repeat
		readln(f,tst);
	until eof(f) or (tst=compareSt);
	yy:=y;
	cLine:=false;
	if not(eof(f)) then repeat
		readln(f,tst);
		if (length(tst)>0) then begin
		 case tst[1] of
		 		'#':begin
		 			cLine:=true;
		 			tst:=copy(tst,2,255);
		 		end;
		 		'_':begin
		 			dec(yy,3);
		 			tst:=copy(tst,2,255);
		 		end;
		 		'*':begin
		 			tst:='';
		 			fonts.outTextCentered(80,yy,continueString);
		 			yy:=y-6;
		 			waitkey;
		 			tg_clear(bgColor);
		 		end;
		 	end;
		 	if not(tst='@end') then fonts.outTextCentered(80,yy,tst);
	  	if cLine then begin
	  		colorize(0,yy,159,yy+6,15,bgColor);
	  		cLine:=false;
	  	end;
	  end;
	  inc(yy,6);
	until eof(f) or (tst='@end');
	close(f);
end;

procedure speedTest;
var
	t,xx,yy:longint;
begin
	t:=0;
	xx:=2;
	yy:=2;
	speedTestResult:=0;
	userClockCounter:=0;
	repeat
    while userTimerExpired(timerInterval) and (t<120) do inc(t);
		inc(speedTestResult);
		tg_bar(xx,yy,xx+5,yy+5,0);
	  tg_tile5(xx,yy,spriteTiles.dataStart,$38+random(8));
	  xx:=xx+6;
	  if (xx>153) then begin
	  	xx:=2;
	  	yy:=yy+6;
	  	if (yy>95) then yy:=2;
	  end;
  until t>=60;
  if ((speedTestResult)<960) and (vSync) then begin
  	vSync:=false;
		graphText(6,'slowpc',4);
  end;
end;

procedure blinkPellets;
var
  t:word;
begin
  pelletBlinkCounter:=(pelletBlinkCounter+1) mod 6;
  case pelletBlinkCounter of
    0:for t:=0 to 3 do with pelletData[t] do begin
      if tileMap[tileY,tileX]=$21 then tg_bar(x,y,x+2,y+2,0);
    end;
    3:for t:=0 to 3 do with pelletData[t] do begin
      if tileMap[tileY,tileX]=$21 then tg_tile3(x,y,mapTiles.dataStart,$21);
    end;
  end;
end;

procedure checkGlobalKeys(ch:char);
begin
	case ch of
	  'o','O':begin
	    if soundOn then killVoices;
	    soundOn:=not(soundOn);
	    showAudioStatus;
	  end;
	  'v','V':begin
	  	vSync:=not(vSync);
	  	showVSyncStatus;
	  end;
	end;
end;

procedure gameLoop;
var
  ch:char;
  t,frameThrottle,altThrottle:word;
  firstRun:boolean;
begin

  pakuSound:=0;
  siren:=0;
  score:=0;
  level:=startingLevel;
  lives:=startingLives;
  ch:=' ';
  firstRun:=true;

  repeat

    dots:=startingDots;

    inc(level);

    if (level>21) then truncLevel:=21 else truncLevel:=level;

    setupPlayfield;

    bonus.reset;

    showReady;

    if firstRun and soundOn then begin
      firstRun:=false;
      playTheme;
      wait(30);
    end else wait(120);

    eraseReady;

    frameSpeed:=levelData[truncLevel].rate;
    frameThrottle:=frameSpeed-1;


    globalJailCounterEnable:=false;
    globalJailCounter:=0;
    globalDotCounter:=0;
    fleeTotal:=0;

    behaviorStep:=0;
    behaviorUpdate;
    altThrottle:=0;
    pelletBlinkCounter:=0;
    gainLifeCounter:=0;

    nextGhost:=200;

    userClockCounter:=0;

    repeat

      if userTimerExpired(timerInterval) then begin

        frameThrottle:=(frameThrottle+1) mod frameSpeed;

        case frameThrottle of
          0:begin
            if vSync then tg_waitRetrace;
            tg_updateSprites;
            player.update;
            fleeTotal:=0;
            for t:=0 to 3 do begin
            	ghost[t].update;
            	if ghost[t].mode=mode_flee then inc(fleeTotal);
            end;
            if (lives>0) then testCollisions;
            case dots of
              fruitCounter1,fruitCounter2:bonus.activate;
            end;
            bonus.update;
          end;
          3:begin
            if vSync then tg_waitRetrace;
            tg_hideSprites;
            blinkPellets;
            tg_showSprites;
          end;
        end;

        if (globalDotCounter<120) then begin
          inc(globalDotCounter);
        end else begin
          globalDotCounter:=0;
          if ghost[2].exitCounter>0 then dec(ghost[2].exitCounter);
          if ghost[3].exitCounter>0 then dec(ghost[3].exitCounter);
        end;

        if (globalFleeCounter>0) then begin
          dec(globalFleeCounter);
          if globalFleeCounter=0 then nextGhost:=200;
        end;

        inc(altThrottle);
        if (altThrottle and $01)=0 then begin
          dec(behaviorCounter);
          if (behaviorCounter=0) then behaviorUpdate;
        end;

        {sound handler }
        if soundOn then begin
          if (frameThrottle and $01)=0 then begin
            siren:=(siren+1) mod 24;
          	if (fleeTotal=0) then begin
          		t:=sirenLookup[siren];
          		case dots of
          			0..43:t:=(t*4) div 3;
          			44..143:t:=(t*5) div 4;
          		end;
	            outFreq(0,t,$0B,0);
	          end else begin
	          	outFreq(0,440-(siren mod 12)*20,$0B,0);
	          end;
          end;
          if (gainLifeCounter>0) then begin
          	dec(gainLifeCounter);
          	case gainLifeCounter of
          		0:outFreq(2,0,0,0);
          		else outFreq(2,660-(gainLifeCounter*11) div 3,$0C,2);
          	end;
          end;
          if (pakuSound>0) then begin
            dec(pakuSound);
            case pakuSound of
              0:outFreq(1,0,0,0);
              else outFreq(1,220+pakusound*$30,$0F,1);
            end;
          end;
        end;

      end; { userTimerExpired }

      if keypressed then begin
        ch:=readkey;
        case ch of
          'w','W','8':player.inputDirection:=0;
          'd','D','6':player.inputDirection:=1;
          's','S','2':player.inputDirection:=2;
          'a','A','4':player.inputDirection:=3;
          #0:begin
            ch:=readkey;
            case ch of
              #$48:player.inputDirection:=0;
              #$4D:player.inputDirection:=1;
              #$50:player.inputDirection:=2;
              #$4B:player.inputDirection:=3;
            end;
          end;
          #27:lives:=0;
          else checkGlobalKeys(ch);
        end;
      end;

    until (lives<=0) or (dots<=0);

    if (soundOn) then begin
      outFreq(0,0,0,0);
      outFreq(1,0,0,0);
    end;

    if (dots=0) then begin
      wait(90);
      tg_hideSprites;
      tg_bar(jail_left,jail_top,jail_right,jail_bottom+5,0);
      frameThrottle:=0;
      player.cSprite^.currentTile:=$43;
      repeat
        wait(30);
        case frameThrottle of
          0,2,4:whitePlayField;
          1,3,5:redrawPlayField;
        end;
        inc(frameThrottle);
      until frameThrottle>6;
      wait(60);
    end;

    tg_hideSprites;

  until (level<=0) or (lives<=0);

end;

procedure drawMenu;
var
  t,n:word;
begin
  level:=0;
  lives:=startingLives;
  userClockCounter:=0;
  tg_clear(0);
  drawPlayField;
  for t:=0 to 7 do tg_tile5(37+t*6,94,spriteTiles.dataStart,t+$38);
  showLives;
  numbers.outDecimal(128,7,score,1);
  numbers.outDecimal(128,1,highScore,1);
  for t:=0 to 5 do begin
    with menuText[t] do fonts.outText(x,y,data);
    n:=91+t*5;
    tg_tile5(n,15,spriteTiles.dataStart,$60+t);
    tg_tile5(n,20,spriteTiles.dataStart,$66+t);
    inc(n,36);
    tg_tile5(n,15,spriteTiles.dataStart,$60+t);
    tg_tile5(n,20,spriteTiles.dataStart,$66+t);
  end;
  tg_tile5(120,19,spriteTiles.dataStart,$50);
  n:=30;
  for t:=0 to 3 do with ghostData[t] do begin
    fonts.outText(98,n,nickname);
    fonts.outText(127,n,behavior);
    colorize(98,n,121,n+4,color or 8,0);
    colorize(127,n,153,n+4,color,0);
    inc(n,7);
  end;
  tg_tile3(101,63,mapTiles.dataStart,$20);
  showAudioStatus;
  showVSyncStatus;
end;

procedure menuLoop;
var
  ch:char;
  t,n,
  counter,
  borderCounter:word;
  sc:longint;
begin
  ch:=#255;
  highScore:=0;
  score:=0;
  counter:=0;
  borderCounter:=0;
  soundOn:=true;
  speedTest;
  drawMenu;
	userClockCounter:=0;
  repeat
    if userTimerExpired(timerInterval) then begin
      counter:=(counter+1) and $1F;
      case counter of
      	$00:begin
		      if vSync then tg_waitRetrace;
          tg_bar(90,30,95,56,0);
          for t:=0 to 3 do tg_tile5(90,30+t*7,spriteTiles.dataStart,t*$08);
      	end;
        $10:begin
		      if vSync then tg_waitRetrace;
          tg_bar(90,30,95,56,0);
          for t:=0 to 3 do tg_tile5(90,30+t*7,spriteTiles.dataStart,1+t*$08);
        end;
        $08,$18:if vSync then tg_waitRetrace;
      end;
      case counter of
        $00,$08,$10,$18:begin
          for t:=0 to 70 do begin
            tg_putpixel(87+t,27,borderColors[borderCounter]);
            tg_putpixel(157-t,58,borderColors[borderCounter]);
            borderCounter:=(borderCounter+1) mod 3;
          end;
          for t:=1 to 31 do begin
            tg_putpixel(87,58-t,borderColors[borderCounter]);
            tg_putpixel(157,27+t,borderColors[borderCounter]);
            borderCounter:=(borderCounter+1) mod 3;
          end;
          inc(borderCounter);
        end;
        $02,$0A,$12,$1A:begin
          blinkPellets;
          case pelletBlinkCounter of
            0:tg_bar(101,69,103,71,0);
            3:tg_tile3(101,69,mapTiles.dataStart,$21);
          end;
        end;
        $0C:begin
          colorize(97,77,116,81,10,0);
          colorize(102,83,112,87,2,0);
        end;
        $1C:begin
          colorize(97,77,116,81,2,0);
          colorize(102,83,112,87,10,0);
        end;
      end;
    end;
    if keypressed then begin
      ch:=readkey;
      if ch=#13 then begin
        gameloop;
        drawMenu;
      end else checkGlobalKeys(ch);
    end;
  until ch=#27;
  killVoices;
end;

begin
  writeln(titleString);
  if paramExists('/?') or paramExists('/help') then begin
  	writeText('help');
  end else begin
  	vSync:=not(paramExists('/nosync'));
	  loadData;
	  tg_init;
	  menuLoop;
	  tg_term;
	  writeln(titleString);
	  cleanup;
	end;
	writeText('disclaimer');
end.